﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="InstallHandler.cs" company="Anton Frattaroli">
//   Copyright (c) Anton Frattaroli. All rights reserved.
// </copyright>
// <summary>
//   Defines the InstallHandler IHttpHanlder implementation class.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Lidocaine
{
    using System;
    using System.Configuration;
    using System.Data.SqlClient;
    using System.Globalization;
    using System.IO;
    using System.Resources;
    using System.Security.Cryptography;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Web;
    using System.Web.Configuration;
    using System.Xml;
    using Lidocaine.Properties;
    using Microsoft.SqlServer.Management.Common;
    using Microsoft.SqlServer.Management.Smo;
    
    /// <summary>
    /// Defines the InstallHandler class.
    /// </summary>
    public class InstallHandler : IHttpHandler
    {
        #region Properties
        /// <summary>
        /// Gets a value indicating whether this implementation of IHttpHandler is reusable.
        /// </summary>
        public bool IsReusable
        {
            get { return false; }
        }
        #endregion

        #region Methods
        /// <summary>
        /// This implementation of IHttpHandler handles one of two asynchronous calls the Lidocaine installation webpage makes.
        /// </summary>
        /// <param name="context">The HttpContext object of the current HTTP request.</param>
        public void ProcessRequest(HttpContext context)
        {
            if (ConfigurationManager.ConnectionStrings["Lidocaine"] == null)
            {
                string databaseUsername = InstallHandler.GetRandomString(8);
                string databasePassword = InstallHandler.GetRandomString(10);
                using (SqlConnection conn = new SqlConnection(string.Format(CultureInfo.InvariantCulture, "Data Source={0};Initial Catalog={1};User ID={2};Password={3};", new object[] { context.Request["databaseServer"], context.Request["databaseName"], string.IsNullOrEmpty(context.Request["userDomain"]) ? context.Request["userName"] : context.Request["userDomain"] + "\\" + context.Request["userName"], context.Request["userPassword"] })))
                {
                    conn.Open();
                    Server server = new Server(new ServerConnection(conn));
                    server.ConnectionContext.ExecuteNonQuery(
                        InstallScript.Install + @"
  CREATE LOGIN [" + databaseUsername + @"] WITH PASSWORD = '" + databasePassword + @"'
  GO
  EXEC('CREATE USER [" + databaseUsername + @"] FOR LOGIN [" + databaseUsername + @"] WITH DEFAULT_SCHEMA = Lidocaine');
  ALTER LOGIN [" + databaseUsername + @"] WITH DEFAULT_DATABASE = [" + context.Request["databaseName"] + @"];
  EXEC sys.sp_addrolemember @rolename=N'LidocaineBots', @membername=N'" + databaseUsername + @"'
  GO
  " + InstallScript.ELMAH);
                }

                XmlDocument existingConfig = new XmlDocument();
                existingConfig.LoadXml(InstallScript.Web_default);
                
                // Lidocaine appConfig
                XmlNode appConfig = existingConfig.SelectSingleNode("/configuration/lidocaine/appConfig");
                appConfig.Attributes["title"].Value = context.Request["siteTitle"];
                appConfig.Attributes["domain"].Value = context.Request["siteDomain"];
                appConfig.ParentNode.ReplaceChild(appConfig, existingConfig.SelectSingleNode("/configuration/lidocaine/appConfig"));

                // Connection String
                XmlNode lidocaineConnectionString = existingConfig.SelectSingleNode("/configuration/connectionStrings/add");
                lidocaineConnectionString.Attributes["connectionString"].Value = string.Format(CultureInfo.InvariantCulture, "Data Source={0};Initial Catalog={1};User ID={2};Password={3};", new object[] { context.Request["databaseServer"], context.Request["databaseName"], databaseUsername, databasePassword });
                lidocaineConnectionString.ParentNode.ReplaceChild(lidocaineConnectionString, existingConfig.SelectSingleNode("/configuration/connectionStrings/add"));

                // Machine Key
                XmlNode machineKey = existingConfig.SelectSingleNode("/configuration/system.web/machineKey");
                machineKey.Attributes["validationKey"].Value = InstallHandler.GetRandomKey(64);
                machineKey.Attributes["decryptionKey"].Value = InstallHandler.GetRandomKey(32);
                machineKey.ParentNode.ReplaceChild(machineKey, existingConfig.SelectSingleNode("/configuration/system.web/machineKey"));
                
                existingConfig.Save(HttpContext.Current.Server.MapPath("~/") + "web.config");
            }
            else
            {
                throw new HttpException("Database installation failed: Connection string named \"Lidocaine\" already exists.");
            }
        }

        /// <summary>
        /// Generates a random key of a certain byte length for the purpose of setting up the MachineKey configuration section.
        /// </summary>
        /// <param name="bytelength">The number of bytes returned (usu. 32 or 64).</param>
        /// <returns>The generated string.</returns>
        private static string GetRandomKey(int bytelength)
        {
            byte[] buff = new byte[bytelength];
            RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
            rng.GetBytes(buff);
            StringBuilder sb = new StringBuilder(bytelength * 2);
            for (int i = 0; i < buff.Length; i++)
            {
                sb.Append(string.Format(CultureInfo.InvariantCulture, "{0:X2}", buff[i]));
            }

            return sb.ToString();
        }

        /// <summary>
        /// Generates a string of random unambiguous alphanumeric characters.
        /// </summary>
        /// <param name="length">The desired length of the returned string.</param>
        /// <returns>The generated string.</returns>
        private static string GetRandomString(int length)
        {
            char[][] charGroups = new char[][] { "abcdefgijkmnopqrstwxyz".ToCharArray(), "ABCDEFGHJKLMNPQRSTWXYZ".ToCharArray(), "23456789".ToCharArray() };
            int[] charsLeftInGroup = new int[charGroups.Length];
            for (int i = 0; i < charsLeftInGroup.Length; i++)
            {
                charsLeftInGroup[i] = charGroups[i].Length;
            }

            int[] leftGroupsOrder = new int[charGroups.Length];
            for (int i = 0; i < leftGroupsOrder.Length; i++)
            {
                leftGroupsOrder[i] = i;
            }

            byte[] randomBytes = new byte[4];
            RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
            rng.GetBytes(randomBytes);
            int seed = (randomBytes[0] & 0x7f) << 24 | randomBytes[1] << 16 | randomBytes[2] << 8 | randomBytes[3];
            Random random = new Random(seed);
            char[] password = null;
            password = new char[length];
            int nextCharIdx;
            int nextGroupIdx;
            int nextLeftGroupsOrderIdx;
            int lastCharIdx;
            int lastLeftGroupsOrderIdx = leftGroupsOrder.Length - 1;
            for (int i = 0; i < password.Length; i++)
            {
                if (lastLeftGroupsOrderIdx == 0)
                {
                    nextLeftGroupsOrderIdx = 0;
                }
                else
                {
                    nextLeftGroupsOrderIdx = random.Next(0, lastLeftGroupsOrderIdx);
                }

                nextGroupIdx = leftGroupsOrder[nextLeftGroupsOrderIdx];
                lastCharIdx = charsLeftInGroup[nextGroupIdx] - 1;
                if (lastCharIdx == 0)
                {
                    nextCharIdx = 0;
                }
                else
                {
                    nextCharIdx = random.Next(0, lastCharIdx + 1);
                }

                password[i] = charGroups[nextGroupIdx][nextCharIdx];
                if (lastCharIdx == 0)
                {
                    charsLeftInGroup[nextGroupIdx] = charGroups[nextGroupIdx].Length;
                }
                else
                {
                    if (lastCharIdx != nextCharIdx)
                    {
                        char temp = charGroups[nextGroupIdx][lastCharIdx];
                        charGroups[nextGroupIdx][lastCharIdx] = charGroups[nextGroupIdx][nextCharIdx];
                        charGroups[nextGroupIdx][nextCharIdx] = temp;
                    }

                    charsLeftInGroup[nextGroupIdx]--;
                }

                if (lastLeftGroupsOrderIdx == 0)
                {
                    lastLeftGroupsOrderIdx = leftGroupsOrder.Length - 1;
                }
                else
                {
                    if (lastLeftGroupsOrderIdx != nextLeftGroupsOrderIdx)
                    {
                        int temp = leftGroupsOrder[lastLeftGroupsOrderIdx];
                        leftGroupsOrder[lastLeftGroupsOrderIdx] = leftGroupsOrder[nextLeftGroupsOrderIdx];
                        leftGroupsOrder[nextLeftGroupsOrderIdx] = temp;
                    }

                    lastLeftGroupsOrderIdx--;
                }
            }

            string strPassword = new string(password);
            return strPassword;
        }
        #endregion
    }
}